#include <stdio.h>      // 标准输入输出库，提供 printf, perror, stdin 等
#include <stdlib.h>     // 标准库，提供 exit, getenv, putenv, atoi, EXIT_FAILURE 等
#include <string.h>     // 字符串处理库，提供 strlen, strcmp, strcpy, strtok, strchr 等
#include <unistd.h>     // POSIX 标准库，提供 fork, getpid, chdir, execvp, sleep 等
#include <ctype.h>      // 字符分类库，提供 isspace 等
#include <sys/types.h>  // 提供一些系统数据类型，如 pid_t, mode_t 等
#include <sys/stat.h>   // 提供文件状态信息，如 stat(), mode_t 等 (虽然代码中 open 直接使用了模式宏)
#include <fcntl.h>      // 提供文件控制函数和宏，如 open(), O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC, O_APPEND 等
#include <sys/wait.h>   // 提供进程等待相关函数和宏，如 waitpid, WIFEXITED, WEXITSTATUS 等

#define SIZE 4096          // 定义一个宏，用于表示命令行的缓冲区大小或路径缓冲区大小
#define MAX_ARGC 64         // 定义一个宏，用于表示命令参数的最大数量 (包括命令名本身)
#define SEP " "             // 定义一个宏，用于表示命令参数的分隔符字符串 (这里是空格)
#define STREND '\0'         // 定义一个宏，表示字符串的结束符 (虽然直接用 '\0' 更常见)

// 下面的宏定义了重定向的类型，使用整数值表示不同的状态
#define NoneRedir  -1       // 没有重定向
#define StdinRedir  0       // 标准输入重定向 (<)
#define StdoutRedir 1       // 标准输出重定向 (>)
#define AppendRedir 2       // 标准输出追加重定向 (>>)

// 定义一个宏，用于跳过字符串中的空白字符
// do{ ... }while(0) 是一个常用的技巧，确保宏在 if/else 语句中也能安全使用，就像一个普通的语句一样结束
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)

int redir_type=NoneRedir;

char* filename=NULL;
char*argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE];
int lastcode=0;

const char* HostName()
{
	char* hostname=getenv("HOSTNAME");
	if(hostname)	return hostname;
	else
		return "None";

}
// 辅助函数：获取用户名
const char* UserName()
{
    // 调用 getenv() 获取 USER 环境变量的值
    char *username = getenv("USER"); // 注意这里变量名应为 username，但代码中使用了 hostname
    // 如果获取成功，返回获取到的用户名字符串指针
    if(username) return username; // 注意这里应返回 username
    // 如果获取失败 (getenv 返回 NULL)，返回一个默认字符串 "None"
    else return "None";
}

// 辅助函数：获取当前工作目录路径
const char *CurrentWorkDir()
{
    // 调用 getenv() 获取 PWD 环境变量的值
    char *cwd = getenv("PWD"); // 注意这里变量名应为 cwd，但代码中使用了 hostname
    // 如果获取成功，返回获取到的当前目录字符串指针
    if(cwd) return cwd; // 注意这里应返回 cwd
    // 如果获取失败 (getenv 返回 NULL)，返回一个默认字符串 "None"
    else return "None";
}

// 辅助函数：获取用户主目录路径
char *Home()
{
    // 调用 getenv() 获取 HOME 环境变量的值
    return getenv("HOME");

}

// 函数：处理交互式用户输入
// out: 用于存储用户输入的命令字符串的缓冲区
// size: 缓冲区的大小
// 返回值：成功读取到的命令字符串的长度 (不包括末尾的 null terminator)，
//        如果读取失败或获取到空字符串，返回 0 或其他表示失败的值 (代码返回长度)
int Interactive(char out[], int size)
{
    // 构建并显示 Shell 命令提示符
    // printf() 函数用于格式化输出
    // 调用 UserName(), HostName(), CurrentWorkDir() 辅助函数获取用户名、主机名和当前目录
    // 输出格式为 "[用户名@主机名 当前目录]$ "
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    // 注意最后的 "$ " 和一个空格，是提示符的一部分

    // 使用 fgets() 函数从标准输入流 (stdin) 读取一行用户输入的命令
    // 读取的内容存储到 out 缓冲区中
    // size 参数限制了最多读取的字符数 (包括可能的换行符和末尾自动添加的 null terminator)
    // fgets 会一直读到换行符('\n')、文件末尾 (EOF) 或读取 size-1 个字符为止
    // 返回值是 out 的地址 (成功) 或 NULL (失败/EOF)
    fgets(out, size, stdin);

    // **** 处理 fgets 读取到的末尾换行符 ****
    // fgets 会将用户按下的回车键生成的换行符('\n') 也读取到缓冲区中
    // 对于命令字符串来说，末尾的换行符是多余的，需要将其移除
    // 通过 strlen(out) 获取当前字符串的长度 (包含换行符)
    // strlen(out)-1 就是换行符 '\n' 的索引位置 (如果字符串以 \n 结尾且 fits in buffer)
    // 将该位置的字符替换为字符串结束符 ('\0')
    // 注意：这种处理方式假设用户输入的一行没有超过缓冲区大小，且以换行符结尾。
    // 如果输入的行过长，没有读取到换行符，这种处理方式会错误地截断倒数第二个字符。
    out[strlen(out)-1] = 0; // 将末尾的换行符替换为 null terminator ('\0')

    // 返回处理后（移除了换行符）的命令字符串的长度
    // 如果用户只按了回车，out 缓冲区将包含 '\n' 然后被替换为 '\0'，strlen 返回 0
    return strlen(out);
}

// 函数：检查并处理命令字符串中的重定向
// in: 指向用户输入的原始命令字符串的指针 (注意：这个字符串会被修改)
void CheckRedir(char in[])
{
    // 示例命令格式：
    // ls -a -l
    // ls -a -l > log.txt      // 标准输出重定向到文件 (覆盖)
    // ls -a -l >> log.txt     // 标准输出追加重定向到文件 (追加)
    // cat < log.txt           // 标准输入重定向从文件读取

    // 初始化全局重定向类型和文件名变量
    // 在处理新命令时，先重置状态
    redir_type = NoneRedir;  // 重定向类型，初始为无重定向
    filename = NULL;       // 重定向的文件名，初始为 NULL

    // 从命令字符串的末尾开始向前扫描，寻找重定向符号
    int pos = strlen(in) - 1; // pos 指向字符串的最后一个字符 (在 null terminator '\0' 之前)

    // 循环从字符串末尾向前移动，直到扫描到字符串开头 (索引 0) 或找到重定向符号并处理
    while( pos >= 0 )
    {
        // 检查当前字符是否是 '>'
        if(in[pos] == '>')
        {
            // 如果是 '>'，进一步检查它是否是 '>>' 的一部分
            // 检查前一个字符是否也是 '>'
            if(pos > 0 && in[pos-1] == '>') // 确保 pos-1 有效
            {
                // 是 '>>' 追加重定向
                redir_type = AppendRedir;
                // 将 '>>' 这两个字符都替换为字符串结束符 '\0'
                // 这样命令字符串在 '>>' 之前就结束了，方便后续 strtok 分割
                in[pos-1] = STREND; // 将 '>' 替换为 '\0'
                in[pos] = STREND;   // 将 '>' 替换为 '\0'
                pos++; // 将 pos 移动到 '>' 字符的下一个位置，也就是重定向文件名的开始位置
                IgnSpace(in, pos); // 跳过重定向符号和文件名之间的空白字符
                filename = in + pos; // 将全局变量 filename 指向重定向文件名开始的位置
                break; // 找到重定向符号并处理完毕，退出循环
            }
            else
            {
                // 是单独的 '>' 输出重定向 (覆盖)
                redir_type = StdoutRedir;
                // 将 '>' 字符替换为字符串结束符 '\0'
                // 这样命令字符串在 '>' 之前就结束了
                in[pos++] = STREND; // 将 '>' 替换为 '\0'，然后将 pos 移动到下一个位置
                IgnSpace(in, pos); // 跳过重定向符号和文件名之间的空白字符
                filename = in + pos; // 将全局变量 filename 指向重定向文件名开始的位置
                break; // 找到重定向符号并处理完毕，退出循环
            }
        }
        // 检查当前字符是否是 '<'
        else if(in[pos] == '<')
        {
            // 是 '<' 输入重定向
            redir_type = StdinRedir;
            // 将 '<' 字符替换为字符串结束符 '\0'
            // 这样命令字符串在 '<' 之前就结束了
            in[pos++] = STREND; // 将 '<' 替换为 '\0'，然后将 pos 移动到下一个位置
            IgnSpace(in, pos); // 跳过重定向符号和文件名之间的空白字符
            filename = in + pos; // 将全局变量 filename 指向重定向文件名开始的位置
            break; // 找到重定向符号并处理完毕，退出循环
        }
        else
        {
            // 如果当前字符不是重定向符号，继续向前扫描前一个字符
            pos--;
        }
    }
    // 循环结束。如果在循环中找到了重定向符号，redir_type 和 filename 会被设置。
    // 如果循环结束都没有找到重定向符号，redir_type 保持 NoneRedir，filename 保持 NULL。
}

// 函数：将命令字符串分割为命令名和参数
// in: 指向已经过 CheckRedir 处理的命令字符串的指针 (注意：这个字符串会被 strtok 修改)
void Split(char in[])
{
    // 首先调用 CheckRedir 处理命令字符串中的重定向
    // CheckRedir 会修改 in 字符串，并在重定向符号处插入 '\0'
    // 同时设置全局变量 redir_type 和 filename
    CheckRedir(in);

    // 接下来使用 strtok 函数将命令字符串分割成 token (命令名和参数)

    int i = 0; // 索引变量，用于在全局 argv 数组中存储 token 指针
    // 用于接收 strtok 返回的 token 指针
    char *token = NULL;

    // **** 第一次调用 strtok ****
    // 将经过 CheckRedir 修改后的命令字符串 in 作为第一个参数传递
    // SEP 是分隔符字符串 " " (这里只包含空格)
    // strtok 会找到第一个 token (通常是命令名)，在其后的第一个分隔符处插入 '\0'，并返回其指针
    token = strtok(in, SEP);

    // 将获取到的第一个 token 指针存入全局 argv 数组的第一个位置 (索引 0)
    argv[i++] = token; // 将 token 指针存入 argv[0]，并将 i 加一

    // **** 后续调用 strtok 的循环 ****
    // 使用 while 循环，不断调用 strtok 获取后续的 token
    // 注意：这里将 NULL 作为 strtok 的第一个参数，告诉它从上次在原始字符串中停下的位置继续扫描
    // strtok 会找到下一个 token，并在其后的第一个分隔符处插入 '\0'，并返回其指针
    // 循环条件：将 strtok(NULL, SEP) 返回的指针赋值给 argv[i++] 后，这个指针不为 NULL
    // 当 strtok(NULL, SEP) 返回 NULL (表示没有更多 token 了) 时，赋值操作 argv[i++] = NULL 发生，
    // 赋值的结果是 NULL，while 条件判断为假，循环终止。
    // 同时，最后一个 NULL 指针被存储到了 argv 数组中所有 token 指针之后的位置。
    // 这正是 execvp 函数所要求的参数数组格式：以 NULL 结尾的字符串指针数组。
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =，这是一个常见的简洁写法

    // 至此，全局变量 argv[] 数组中存储了命令名和参数的指针，以 NULL 结尾。
    // argv[0] 是命令名，argv[1]...argv[i-2] 是参数，argv[i-1] 是 NULL。

    // **** 特殊处理 ls 命令 ****
    // 这部分代码是专门为 ls 命令添加额外选项的例子
    if(strcmp(argv[0], "ls") == 0) // 检查命令名是否是 "ls"
    {
        // 如果是 ls 命令，为其添加一个 "--color" 选项
        // 在 strtok 分割后，argv 数组的最后一个有效参数指针后面跟着 NULL
        // argv[i-1] 是 NULL 指针
        // argv[i-2] 是最后一个有效参数指针
        // 这里的逻辑 argv[i-1] = (char*)"--color"; 意味着覆盖了原本的 NULL 指针
        // argv[i] = NULL; 再在新的参数后面添加一个 NULL 指针
        // 这段代码的目的是在原有参数列表的末尾添加 "--color" 和新的 NULL 结束符
        // 例如，如果原始命令是 "ls -l"，分割后 argv 是 {"ls", "-l", NULL}
        // i 此时为 3 (对应 argv[0], argv[1], argv[2]=NULL)
        // argv[i-1] = argv[2] = NULL
        // argv[2] = (char*)"--color"; // argv 现在是 {"ls", "-l", "--color"}
        // i 变成了 4
        // argv[4] = NULL; // argv 现在是 {"ls", "-l", "--color", NULL}
        // 如果原始命令是 "ls"，分割后 argv 是 {"ls", NULL}
        // i 此时为 2 (对应 argv[0], argv[1]=NULL)
        // argv[i-1] = argv[1] = NULL
        // argv[1] = (char*)"--color"; // argv 现在是 {"ls", "--color"}
        // i 变成了 3
        // argv[3] = NULL; // argv 现在是 {"ls", "--color", NULL}
        // 这种写法是有效的，利用了 strtok 保证数组以 NULL 结尾的特性，并在 NULL 的位置插入新参数，再添加新的 NULL。
        argv[i-1] = (char*)"--color"; // 在原参数列表末尾添加 "--color" 参数
        argv[i] = NULL;               // 在新参数后添加新的 NULL 结束符，保证数组以 NULL 结尾
    }
}

// 函数：检查并处理内建命令
// argv: 指向命令名和参数字符串数组的指针，以 NULL 结尾
// 返回值：1 表示命令是内建命令并已被处理，0 表示命令不是内建命令
int BuildinCmd(char *argv[])
{
    // ret 变量用于标记是否处理了内建命令。初始为 0 (不是内建命令)
    int ret = 0;

    // 1. 检测 argv[0] 是否与已知的内建命令名匹配
    // 使用 strcmp 函数比较字符串
    if(strcmp("cd", argv[0]) == 0)
    {
        // 2. 如果是 "cd" 命令，执行其逻辑
        ret = 1; // 设置返回值为 1，表示这是一个内建命令且将被处理

        char *target = argv[1]; // cd 命令的第一个参数，表示目标目录
        // 检查用户是否指定了目标目录 (即 argv[1] 是否为 NULL)
        if(!target) { // 如果没有指定目标目录 (argv[1] 是 NULL)
            // 按照 Shell 约定，cd 后没有参数表示切换到用户的主目录
            target = Home(); // 调用辅助函数 Home() 获取用户主目录的路径
            // 注意：Home() 可能返回 NULL，实际应用中需要处理这种情况
             if (target == NULL) {
                 fprintf(stderr, "cd: HOME not set\n"); // 如果 HOME 未设置，报告错误
                 // 可以选择不切换目录，或者切换到 / 目录等
                 // return 1; // 返回 1 表示处理了 cd 命令 (虽然失败了)
                 return 0; // 示意处理失败但不继续作为外部命令
             }
        }

        // 调用 chdir() 系统调用，实际改变当前进程的工作目录
        // chdir() 成功返回 0，失败返回 -1 并设置 errno
        if(chdir(target) == -1) {
            // 如果 chdir 失败，打印错误信息 (如目录不存在、权限不足等)
            perror("cd failed");
            // cd 命令执行失败，可以返回一个非零状态码
            // 这里 BuildinCmd 返回 1 只表示处理了 cd 命令，其自身的成功/失败状态需要另外记录
        } else {
            // **** chdir 成功后，更新 PWD 环境变量 ****
            // chdir 系统调用不会自动更新 Shell 的 PWD 环境变量
            // 需要手动获取新的当前目录路径，并更新 PWD
            char temp[1024]; // 临时缓冲区，用于存储获取到的当前目录路径
            // getcwd() 获取当前工作目录的绝对路径，存入 temp 缓冲区
            if(getcwd(temp, 1024) != NULL) { // 检查 getcwd 是否成功
                 // 使用 snprintf 格式化构建 "PWD=新的目录路径" 字符串，存入全局变量 pwd 数组
                 // 这是为了 putenv 函数的需要，它通常需要一个持久存在的字符串
                 snprintf(pwd, SIZE, "PWD=%s", temp);
                 // 调用 putenv() 函数，将 "PWD=新的目录路径" 添加或更新到当前进程的环境变量列表中
                 // putenv() 会将 pwd 数组中的内容添加到环境，或者更新同名变量的值
                 putenv(pwd);
                 // cd 命令执行成功
             } else {
                 perror("getcwd failed after cd"); // 如果 getcwd 失败，报告错误
             }
        }
    
	}
	else if(strcmp("export",argv[0])==0)
	{
		ret=1;
		if(argv[1])
		{
			strcpy(env,argv[1]);
			putenv(env);
		}
	}
	else if(strcmp("echo",argv[0])==0)
	{
		ret=1;
		if(argv[1]==NULL)
			printf("\n");
		else
		{
			if(argv[1][0]=='$')
			{
				
				// 如果以 '$' 开头，可能是环境变量引用
                // 检查是否是 "$?" (表示上一个命令的退出状态码)
                if(argv[1][1] == '?') // 检查第二个字符是否是 '?'
                {
                    // 如果是 "$?"，打印全局变量 lastcode 的值
                    // lastcode 存储着上一个执行的命令的退出状态
                    printf("%d\n", lastcode);
                    // 注意：标准 Shell 在执行内建命令后，通常会将 $? 设置为 0 (如果内建命令执行成功)
                    // 这里简单地将 lastcode 设置为 0，模拟成功状态
                    // lastcode = 0; // 这一行在提供的代码片段中缺失，但逻辑上需要
                }
                else{
                    // 如果不是 "$?"，而是 "$变量名" 的格式
                    // 获取变量名 (跳过第一个 '$' 字符)
                    char *var_name = argv[1] + 1; // 指针前移一位，跳过 '$'

                    // 调用 getenv() 获取对应环境变量的值
                    char *e = getenv(var_name);
                    // 如果 getenv 返回非 NULL (环境变量存在)
                    if(e) {
                        printf("%s\n", e); // 打印环境变量的值
                    }
                    // 如果 getenv 返回 NULL (环境变量不存在)，标准 Shell 通常不输出任何东西，这里也是如此。
                }
			}
			else
			{
				printf("%s\n",argv[1]);
			}
	 		
		}
	
	
	}
	//其他..
	return ret;
}

void Execute()
{
	pid_t id=fork();
	if(id==0)
	{	
		int fd=-1;
		if(redir_type==StdinRedir)
		{
			fd=open(filename,O_RDONLY);
			if(fd<0)
			{
				perror("open input file failed");
				exit(1);
			}
			dup2(fd,0);
			close(fd);
		}
	
		else if(redir_type==StdoutRedir)
			{
			fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
			dup2(fd,1);
			close(fd);
			}

		else if(redir_type == AppendRedir) // 如果是标准输出追加重定向 (>>)
		{
            // 使用 open() 函数打开重定向文件 (filename)，以写模式 O_WRONLY
            // O_CREAT 表示如果文件不存在则创建它
            // O_APPEND 表示如果文件已存在，则将写入的内容追加到文件末尾
             fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
              if (fd < 0) { // 文件打开失败
                 perror("open append file failed");
                 exit(1); // 子进程异常退出
            }
            // 使用 dup2() 函数将文件描述符 fd 复制到标准输出的文件描述符 (1)
            // 这会将当前进程的标准输出重定向到打开的文件 (并设置了追加模式)
            dup2(fd, 1);
            close(fd); // 释放文件资源
		}
		execvp(argv[0],argv);
		perror("execvp failed");
		exit(1);
	}
	else if(id>0)
	{
		int status=0;
		pid_t rid=waitpid(id,&status,0);
		if(rid==id)
		{
			if(WIFEXITED(status))
			{
				lastcode=WEXITSTATUS(status);

			}
			else if(WIFSIGNALED(status)){
				lastcode=128+WTERMSIG(status);
			}
		}
	}

}
int main()
{
	while(1)
	{
		char commandline[SIZE];
		int n=0;
		n=Interactive(commandline,SIZE);
		if(n==0)
		{
			continue;
		}
		Split(commandline);
		n=BuildinCmd(argv);
		if(n)
		{
			continue;
		}

		Execute();
	}
	return 0;
}
